package entities;

import entities.exceptions.CheckpointNotAtRouteException;
import entities.exceptions.EmptyRouteException;
import entities.exceptions.RouteIndexOutOfRangeException;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import willispace.Map;

/**
 * Route Class
 * @author Mateusz
 */
public class Route {

    /**
     * list of tiles
     */
    protected ArrayList<Tile> route;

    /**
     * creates new empty route
     */
    public Route() {
        route = new ArrayList<Tile>();
    }

    /**
     * Creates route from start tile to end tile
     * @param startTile start checkpoint
     * @param endTile   end checkpoint
     */
    public Route(Tile startTile, Tile endTile) {
        this();
        this.route = getRoute(startTile, endTile);

        // set all tiles as "on route"
        Iterator it = route.iterator();
        while (it.hasNext()) {
            Tile t = (Tile) it.next();
            t.increaseRoutes();
        }
    }

    /**
     * copying constructor
     * @param r route to copy
     */
    public Route(Route r) {
        route = (ArrayList<Tile>) r.route.clone();
    }

    /**
     * destroys route
     */
    public void destroy() {
        // set all tiles at "not at this route anymore"
        Iterator it = route.iterator();
        while (it.hasNext()) {
            Tile t = (Tile) it.next();
            t.decreaseRoutes();
        }
    }

    /**
     * adds single checkpoint to route
     * @param t checkpoint to add
     */
    public void addCheckpoint(Tile t) {
        route.add(t);
    }

    /**
     * merge this route with another one
     * @param ar    array list of tiles to add
     */
    public void appends(ArrayList<Tile> ar) {
        route.addAll(ar);
    }

    /**
     * merge this route with another one
     * @param r route to merge with
     */
    public void appends(Route r) {
        route.addAll(r.route);
    }

    /**
     * get next checkpoint after pointed
     * @param t current checkpoint
     * @return  next checkpoint
     * @throws RouteIndexOutOfRangeException    there is no more checkpoints
     */
    public Tile getNextCheckpoint(Tile t) throws RouteIndexOutOfRangeException {
        int pos = route.indexOf(t);
        if (pos + 1 < route.size()) {
            return route.get(pos + 1);
        }
        throw new RouteIndexOutOfRangeException();
    }

    /**
     * get next checkpoint after pointed
     * @param i current checkpoint index
     * @return  next checkpoint
     * @throws RouteIndexOutOfRangeException    there is no more checkpoints
     */
    public Tile getCheckpoint(int i) throws RouteIndexOutOfRangeException {
        try {
            return route.get(i);
        } catch (Exception e) {
            throw new RouteIndexOutOfRangeException();
        }
    }

    /**
     * deletes checkpoint
     * @param i                                 index
     * @param decreaseRoutesCounter             if set tile as not at this route
     * @throws RouteIndexOutOfRangeException    there is no such checkpoint at route
     */
    public void deleteCheckpoint(int i, boolean decreaseRoutesCounter) throws RouteIndexOutOfRangeException {
        try {
            Tile t = route.remove(i);
            if (decreaseRoutesCounter) {
                t.decreaseRoutes();
            }
        } catch (IndexOutOfBoundsException e) {
            throw new RouteIndexOutOfRangeException();
        }
    }

    /**
     * deletes checkpoint
     * @param t                                 checkpoint
     * @param decreaseRoutesCounter             if set tile as not at this route
     * @throws RouteIndexOutOfRangeException    there is no such checkpoint at route
     */
    public void deleteCheckpoint(Tile t, boolean decreaseRoutesCounter) throws CheckpointNotAtRouteException {
        boolean removed = route.remove(t);

        if (!removed) {
            throw new CheckpointNotAtRouteException();
        }
        if (decreaseRoutesCounter) {
            t.decreaseRoutes();
        }
    }

    /**
     * reverse route
     */
    public void reverse() {
        Collections.reverse(route);
    }

    private Tile top(boolean remove) throws EmptyRouteException {
        try {
            Tile t = getCheckpoint(0);
            if (remove) {
                deleteCheckpoint(t, false);
            }
            return t;
        } catch (Exception e) {
            throw new EmptyRouteException();
        }
    }

    /**
     * gets route start checkpoint
     * @return start checkpoint
     * @throws EmptyRouteException  if route is empty
     */
    public Tile getStart() throws EmptyRouteException {
        return top(false);
    }

    /**
     * gets route end checkpoint
     * @return  end checkpoint
     * @throws EmptyRouteException  if route is empty
     */
    public Tile getEnd() throws EmptyRouteException {
        try {
            return getCheckpoint(getRouteLength() - 1);
        } catch (RouteIndexOutOfRangeException e) {
            throw new EmptyRouteException();
        }
    }

    /**
     * 
     * @return route length
     */
    public int getRouteLength() {
        return route.size();
    }

    /**
     * 
     * @return copy of route
     */
    public Route copy() {
        return new Route(this);
    }

    @Override
    public String toString() {
        String s = "";
        Iterator it = route.iterator();
        while (it.hasNext()) {
            Tile t = (Tile) it.next();
            s = s.concat("[" + t.getIndexes().x + ", " + t.getIndexes().y + "] ");
        }
        return s;
    }

    /**
     * Calculates route from startTile to endTile
     * @param startTile start checkpoint
     * @param endTile   end checkpoint
     * @return          list of tiles on route
     */
    public static ArrayList<Tile> getRoute(Tile startTile, Tile endTile) {
        Map map = willispace.WilliSpace.getMap();
        ArrayList<Tile> tmpRoute = new ArrayList<Tile>();
        Point[] directions = {new Point(0, 1), new Point(0, -1), new Point(1, 0), new Point(-1, 0),
            new Point(-1, -1), new Point(-1, 1), new Point(1, -1), new Point(1, 1)};
        ArrayList<Tile> visited = new ArrayList<Tile>();
        HashMap<Tile, Tile> ancestors = new HashMap<Tile, Tile>();
        Queue q = new LinkedList();

        q.add(startTile);
        ancestors.put(startTile, null);
        visited.add(startTile);

        outer:
        while (!q.isEmpty()) {
            Tile t = (Tile) q.remove();
            Tile t2;
            Point tileXY = t.getIndexes();

            for (Point d : directions) {
                // if in map range
                if (properTileXY(tileXY.x + d.x, tileXY.y + d.y, map)) {
                    t2 = map.getTile(tileXY.x + d.x, tileXY.y + d.y);

                    if (!t2.isBlocked() || t2 == endTile) {
                        if (!visited.contains(t2)) {
                            q.add(t2);
                            ancestors.put(t2, t);
                            visited.add(t2);
                        }

                        // if end checkpoint
                        if (t2 == endTile) {
                            break outer;
                        }
                    }
                }
            }
        }

        // regeneration of route
        Tile t = endTile;
        while (t != null) {
            tmpRoute.add(t);
            t = ancestors.get(t);
        }

        // reverse tmpRoute
        Collections.reverse(tmpRoute);

        return tmpRoute;
    }

    /**
     * 
     * Check if point (x,y) is in map range
     */
    private static boolean properTileXY(int x, int y, Map map) {
        return x >= 0 && x < map.getWidth() && y >= 0 && y < map.getHeight();
    }

    private static boolean properTileXY(Point p, Map map) {
        return properTileXY(p.x, p.y, map);
    }
}
